package edu.mercer.sse658.amazonbkp;

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;

import org.codehaus.jackson.JsonFactory;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.JsonParser.Feature;
import org.codehaus.jackson.map.ObjectMapper;

import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.policy.Policy;
import com.amazonaws.auth.policy.Principal;
import com.amazonaws.auth.policy.Resource;
import com.amazonaws.auth.policy.Statement;
import com.amazonaws.auth.policy.Statement.Effect;
import com.amazonaws.auth.policy.actions.SQSActions;
import com.amazonaws.services.glacier.AmazonGlacierClient;
import com.amazonaws.services.glacier.model.GetJobOutputRequest;
import com.amazonaws.services.glacier.model.GetJobOutputResult;
import com.amazonaws.services.glacier.model.InitiateJobRequest;
import com.amazonaws.services.glacier.model.InitiateJobResult;
import com.amazonaws.services.glacier.model.JobParameters;
import com.amazonaws.services.sns.AmazonSNSClient;
import com.amazonaws.services.sns.model.CreateTopicRequest;
import com.amazonaws.services.sns.model.CreateTopicResult;
import com.amazonaws.services.sns.model.SubscribeRequest;
import com.amazonaws.services.sns.model.SubscribeResult;
import com.amazonaws.services.sqs.AmazonSQSClient;
import com.amazonaws.services.sqs.model.CreateQueueRequest;
import com.amazonaws.services.sqs.model.CreateQueueResult;
import com.amazonaws.services.sqs.model.GetQueueAttributesRequest;
import com.amazonaws.services.sqs.model.GetQueueAttributesResult;
import com.amazonaws.services.sqs.model.Message;
import com.amazonaws.services.sqs.model.ReceiveMessageRequest;
import com.amazonaws.services.sqs.model.SetQueueAttributesRequest;
import com.google.common.base.Optional;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.hash.Hashing;
import com.google.common.io.Files;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListenableFutureTask;
import com.google.common.util.concurrent.SimpleTimeLimiter;
import com.google.common.base.Stopwatch;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

/**
 * Amazon Task: an AWS Glacier job run in another thread.<p>
 *
 * Return Boolean success of AWS job return code. Uses ListenableFuture
 * 	for callback upon success: subsequently download complete job output.<p>
 */

public class AmazonTask implements ListenableFuture<AmazonTask>,
								   Observer,
								   ConfigConstants,
								   Callable<AmazonTask>
{
	private Optional<AWSTaskBuilder> _builder = Optional.absent();

	private Boolean _cancel = false;

	private Boolean _cancelNow = false;
	
	private Boolean _finished = false;
	
	private Boolean _abend = false;
	
	private Boolean _taskOK = false;
	
	private Boolean _started = false;

	final private Set<Runnable> _listeners = new HashSet<>();
	
	private Optional<Executor> _executor = Optional.absent();
	
	private Optional<JobID> _jobID = Optional.absent();
	
	private Optional<TaskOutput> _taskOutput = Optional.absent();
	
	private Optional<List<Message>> _queueMsgs = Optional.absent();

	
	public AmazonTask( final AWSTaskBuilder builderIn )
	{
		super();
		
		checkNotNull( builderIn, "Null detected Builder argument" );
		
		_builder = Optional.of( builderIn );
		
		if( _builder.get()
					.jobID()
					.isPresent() )
		{
			System.out.println( "job id from builder: " + 
					_builder.get().jobID() );
			
			LogCustom.obtainLog( "job_log", Level.INFO).info(
					"job id provided: " + _builder.get().jobID() );
			
			_jobID = _builder.get()
							 .jobID();
			
		}//if
		
	}//constructor
	
	
	public static AWSTaskBuilder builder()
	{
		return new AWSTaskBuilder();
		
	}//method
	
	
	@Fulfills("ListenableFuture")
	@Override
	public boolean cancel( final boolean arg0 ) 
	{
		_cancelNow = arg0;
		
		_cancel = true;
		
		return !isDone();
		
	}//method
	
	
	@Fulfills("ListenableFuture")
	@Override
	public AmazonTask get() throws InterruptedException, 
			   					   ExecutionException 
	{
		try
		{
			_taskOK = performTask().ranOK();
			
		}
		catch( final IOException err )
		{
			throw new ExecutionException( err );
			
		}//try
		
		return this;
	
	}//method
	

	@Fulfills("ListenableFuture")
	@Override
	public AmazonTask get( final long arg0, 
						   final TimeUnit arg1 ) 
		throws InterruptedException, 
			   ExecutionException, 
			   TimeoutException 
	{

		SimpleTimeLimiter limiter = new SimpleTimeLimiter();
        
		limiter.newProxy( this, 
				ListenableFuture.class, 
				ConfigConstants.JOBS.TIMEOUT.asLong(), 
				TimeUnit.MINUTES ).get();
		
		_taskOK = ranOK();
		
		return this;
                
	}//method


	@Fulfills("Callable")
	@Override
	public AmazonTask call() throws Exception 
	{
		return get();
	
	}//method
	
	
	@Override
	public boolean isCancelled() 
	{
		return ( _cancel || _cancelNow) && isDone();
	
	}//method


	@Fulfills("ListenableFuture")	
	@Override
	public boolean isDone() 
	{
		return _abend || _finished;
	
	}//method


	@Fulfills("ListenableFuture")	
	@Override
	public void addListener( final Runnable listener, 
							 final Executor executor ) 
	{
		_listeners.add( listener );
		
		_executor = Optional.of( executor );

		/* execute immediately if task has already completed */
		
		if( isDone() )
		{
			summonListeners();
			
		}//if
		
	}//method
	
	
	public AWSTaskBuilder builderUsed(){ return _builder.get(); } 

	
	private void summonListeners()
	{
		if( !_executor.isPresent() || _listeners.size() < 1 )
		{
			return;
			
		}//if
		
		for( Runnable currentListener : _listeners )
		{ 
			_executor.get().execute( currentListener );
		
		}//for
		
	}//method
	
	
	public Boolean abended(){ return _abend; }
	
	
	public Boolean ranOK()
	{
		return !abended() && isDone();
		
	}//method
	
	
	/**
	 * Run the AWS Glacier job, polling for interruption.<p>
	 * 
	 * Intended for use from get() and similar of Future and variants.<p>
	 * 
	 * @return this
	 * @throws InterruptedException 
	 * @throws IOException 
	 * @throws JsonParseException 
	 */
	
	private AmazonTask performTask() 
			
		throws InterruptedException, 
			   JsonParseException,
			   IOException
	{
		/* do not allow same task to be run twice, repeated calls to get */
		
		if( !_started )
		{ 
			_started = true;
			
		}
		else
		{
			return this;
			
		}//if
		
		if( _cancel ) return this;
			
		final JobParameters jobParameters = _builder.get().jobParameters();

		final String vaultName = _builder.get().vault();
		
		final AmazonGlacierClient glacierClient = _builder.get()
														  .glacierclient();
		
		final AmazonSQSClient queueClient = _builder.get().queueClient();
		
		final String queueURL = _builder.get().queueURL();
		
		/* 
		 * job, job-monitoring may be resumed
		 * 
		 * do not start another new job if job id provided
		 */
		
		if( !_jobID.isPresent() || _jobID.get().toString().length() < 1 )
		{		
			LogCustom.obtainLog("job_log", Level.INFO).info(
					"starting new job");			
			
			final InitiateJobRequest request = new InitiateJobRequest()
					.withVaultName( vaultName )
					.withJobParameters( jobParameters );
			
			final InitiateJobResult response = glacierClient
					.initiateJob(request);

			_jobID = Optional.of( 
					new JobID().fromString( response.getJobId() ) );
			
			checkArgument( _jobID.isPresent() || 
						  _jobID.get().toString().length() < 1, 
					"remote aws job id not obtained" );
			
			System.out.println( "job id obtained: " + jobID() );

			LogCustom.obtainLog("job_log", Level.INFO).info(
					"job id obtained: " + jobID() );			
			
		}
		else
		{
			LogCustom.obtainLog("job_resume", Level.WARNING)
					 .warning( "resuming job id: " + _jobID.get() );
			
		}//if

		/* observer, observable usage to record the aws remote job id */

		_jobID.get().addObserver( this );

		_jobID.get().setChanged();
		
		_jobID.get().notifyObservers();
		
		/* wait for job to complete */

		List<Message> jobsMessages = Collections.emptyList();
		
		do
		{     
			if( _cancelNow ) interruptTask( jobID() );

			/* attempt to retrieve job status messages */
			
			jobsMessages = queueClient
					
					.receiveMessage(					
							new ReceiveMessageRequest( queueURL )
								.withMaxNumberOfMessages(10) 		
								   )
					.getMessages();        	

			if( jobFinished( jobsMessages, jobID() ) ) break;
			
			/* sleep */
			
			dozeFor( jobID() );
			
			/* check if job finished yet */
			
		}while( !jobFinished( jobsMessages, jobID() ) );
		
		return this;
		
	}//method
	
	
	/**
	 * Sleep using Guava stopwatch, polling for interruption.<p>
	 * 
	 * Accepts jobID parameter, used for exception description upon 
	 * 	interruption.
	 * 
	 * @param jobID - JobID representing AWS job ID String
	 * 
	 * @throws InterruptedException
	 */
	
	private void dozeFor( final JobID jobID ) 
		throws InterruptedException
	{
		Stopwatch stopWatch = Stopwatch.createUnstarted();

		stopWatch.start();

		do
		{
			if( _cancelNow ) interruptTask( jobID() );
			
			Thread.sleep( 1000L );

		}while( stopWatch.elapsed( TimeUnit.SECONDS ) < JOBS.POLL.asLong() );

		stopWatch.stop();
		
	}//method
	
	
	private void interruptTask( final JobID jobID ) 
		throws InterruptedException
	{
		_abend = true;
		
		throw new InterruptedException( "job ID: " + jobID + " cancelled");		
		
	}//method
	

	/**
	 * Parses aws queue-notifications retrieved and supplied to determine if 
	 * 	desired job has completed.<p>
	 * 
	 * If job found as completed in queue message, calls method to run 
	 * 	any listeners.<p>
	 * 
	 * @param jobMsgs - List of aws Message objects
	 * 
	 * @param jobID - String job ID of remote aws job
	 * 
	 * @return Boolean - T if remote aws job has finished
	 * 
	 * @throws JsonParseException
	 * @throws InterruptedException
	 * @throws IOException
	 */
	
	private Boolean jobFinished( final List<Message> jobMsgs, 
								 final JobID jobID ) 
		throws JsonParseException, 
			   InterruptedException,
			   IOException
	{
		if( _queueMsgs.isPresent() && 
			_queueMsgs.get()
					  .containsAll(jobMsgs) )
		{
			/* no new messages */
			
			return false;
	
		}
		else if( ! _queueMsgs.isPresent() )
		{
			/* 1st time: create instance variable to contain any messages */
			
			ArrayList<Message> emptyMsgs = new ArrayList<>();
			
			emptyMsgs.ensureCapacity( jobMsgs.size() );
			
			List<Message> emptyLst = emptyMsgs;
			
			_queueMsgs = Optional.of( emptyLst ); 
			
		}//if
		
		final ObjectMapper mapper = new ObjectMapper();
		final JsonFactory factory = mapper.getJsonFactory();
		
        for( Message m : jobMsgs ) 
        {
        	if( ! _queueMsgs.get().contains(m) )
        	{
        		_queueMsgs.get().add(m);
        		
        		LogCustom.obtainLog("aws_queue_msgs", Level.INFO)
        				 .info( m.toString() );

        		if( _cancelNow ) interruptTask( jobID );

        		String jobMessage = mapper.readTree( factory

        				.createJsonParser( m.getBody() ) )

        				.get("Message")
        				.getTextValue();

        		JsonNode jobDescNode = mapper
        								.readTree( factory
        								.createJsonParser( jobMessage ) );

        		String retrievedJobId = jobDescNode.get("JobId")
        										   .getTextValue();
        		
        		String statusCode = jobDescNode.get("StatusCode")
        									   .getTextValue();

        		if( retrievedJobId.equals( jobID.toString() ) ) 
        		{
        			LogCustom.obtainLog("aws_queue_msgs", Level.INFO)
        			.info("located job id in queue messages");

        			if( !statusCode.equals("Succeeded") ) 
        			{
        				_abend = true;

        				LogCustom.obtainLog("job_completion")
        						 .severe("abended");

        			}//if

        			LogCustom.obtainLog("job_completion", Level.INFO).info( 
        					"finished: " + jobID );

        			_finished = true;

        			summonListeners();

        			return true;

        		}//if
        	}//if
        }//for

		return false;
		
	}//method

	
	private Boolean jobFinished( final List<Message> jobMsgs) 
			throws JsonParseException, 
				   InterruptedException, 
				   IOException
	{
		return jobFinished( jobMsgs, jobID() );
		
	}//method
	
	
	public JobID jobID()
	{
		return _jobID.or( new JobID() );
		
	}//method
	
	
	/**
	 * Allow resume for job id already known.<p>
	 * 
	 * @param jobIDIn
	 * 
	 * @return
	 */
	
	public JobID jobID( final JobID jobIDIn )
	{
		checkNotNull( jobIDIn, "null detected job id argument" );
		
		_jobID = Optional.of( jobIDIn );
		
		return _jobID.get();
		
	}//method
	
	
	public Optional<TaskOutput> taskOutput() throws InterruptedException,
													ExecutionException
	{
		return _taskOutput;
		
	}//method
	
	
	public AmazonTask taskOutput( final TaskOutput taskOutputIn )
	{
		_taskOutput = Optional.of( taskOutputIn );
		
		return this;
		
	}//method


	@Fulfills("Observer")
	@Override
	public void update( final Observable observableIn, 
						final @Nullable Object argIn ) 
	{
		if( observableIn instanceof JobID )
		{
			JobID theID = (JobID) observableIn;
			
			String logMsg = theID.toString();
			
			if( argIn != null ) logMsg += " " + argIn;
			
			LogCustom.obtainLog( "aws_job_ids", Level.INFO )
					 .info( logMsg );
			
		}//if
		
	}//method


}//class


/**
 * AmazonTask Builder.<p>
 *
 */

class AWSTaskBuilder
{
	private Optional<String>     	      _vaultName     = Optional.absent();
	
	private Optional<JobParameters> 	  _jobParameters = Optional.absent();
	
	private Optional<AmazonGlacierClient> _glacierClient = Optional.absent();
	
	private Optional<AmazonSQSClient>     _queueClient   = Optional.absent();
	
	private Optional<String>			  _queueURL		 = Optional.absent();
			
	private Optional<JobID>				  _jobID		 = Optional.absent();

	
	AWSTaskBuilder()
	{
		super();
		
	}//constructor

	
	public String vault(){ return _vaultName.get(); }
	
	
	public AWSTaskBuilder vault( final String vaultName )
	{
		checkNotNull( vaultName, "Null detected vault name argument" );
		
		_vaultName = Optional.of( vaultName );
		
		return this;
		
	}//method


	public JobParameters jobParameters(){ return _jobParameters.get(); }
	

	public AWSTaskBuilder jobParameters( final JobParameters jobParm )
	{
		checkNotNull( jobParm, "Null detected job parameters argument" );
		
		_jobParameters = Optional.of( jobParm );
		
		return this;
		
	}//method

	
	public AmazonGlacierClient glacierclient()
	{ 
		return _glacierClient.get(); 
		
	}//method
	
	
	public AWSTaskBuilder glacierclient( final AmazonGlacierClient clientIn )
	{
		checkNotNull( clientIn, 
				"Null detected AmazonGlacierClient argument" );
		
		_glacierClient = Optional.of( clientIn );
		
		return this;
		
	}//method
	
	
	public AmazonSQSClient queueClient()
	{
		return _queueClient.get();
		
	}//method
	
	
	public AWSTaskBuilder queueClient( final AmazonSQSClient clientIn )
	{
		_queueClient = Optional.of( clientIn );
		
		return this;
		
	}//method
	
	
	public String queueURL()
	{
		return _queueURL.get();
		
	}//method
	
	
	public AWSTaskBuilder queueURL( final String queueURLIn )
	{
		_queueURL = Optional.of( queueURLIn );
		
		return this;
		
	}//method
	
	
	public AWSTaskBuilder jobID( final JobID jobID )
	{
		checkNotNull( jobID, "null detected job id argument" );
		
		_jobID = Optional.of( jobID );
		
		return this;
		
	}//method
	
	
	public Optional<JobID> jobID(){ return _jobID; }
	
	
	public AmazonTask build()
	{
		checkArgument( _vaultName    .isPresent(), 
				"Vault name not set" );
		
		checkArgument( _jobParameters.isPresent(), 
				"Job parameters object not set" );
		
		checkArgument( _glacierClient.isPresent(), 
				"Amazon Glacier client not set" );

		checkArgument( _queueClient.isPresent(),
				"Queue client not set" );
		
		checkArgument( _queueURL.isPresent(),
				"Queue URL not set" );
				
		return new AmazonTask(this);
		
	}//method

	
}//class


/**
 * Callback to retrieve job output.<p>
 *
 * Upon job completion, as added listener in ListenableFuture.<p>
 * 
 * May be accessed from thread running using outer future class. 
 * 	Thread safe collection used.<p>
 */

class TaskOutput implements Runnable
{
	private CopyOnWriteArrayList<String> _lines = 
			new CopyOnWriteArrayList<>();
	
	private JobID _jobID = new JobID();
	
	private String _vaultName = "";
	
	private Optional<AmazonGlacierClient> _glacierClient = Optional.absent();
	
	private Optional<AmazonTask> _task = Optional.absent();
	
	
	public TaskOutput( final AmazonTask taskIn ) 
			throws InterruptedException, 
				   ExecutionException
	{
		final String vaultName = taskIn.builderUsed()
									   .vault();
		
		final JobID jobID = taskIn.jobID();
		
		final AmazonGlacierClient glacierClient = taskIn.builderUsed()
														.glacierclient();		
		
		checkNotNull( vaultName, "null detected vault name" );
		
		checkNotNull( jobID, "null detected job ID" );
		
		_vaultName = vaultName;
		
		_jobID = jobID;
		
		_glacierClient = Optional.of( glacierClient );
		
		_task = Optional.of( taskIn );
		
	}//constructor
	
	
	private TaskOutput retrieveLines()
	{
		LogCustom.obtainLog("job_output", Level.INFO)
			.info("retrieve lines method called");
		
		_lines.clear();
		
        final GetJobOutputRequest getJobOutputRequest = new 
        		GetJobOutputRequest()
        		.withVaultName( _vaultName )
        		.withJobId( _jobID.toString() );
        
        final GetJobOutputResult getJobOutputResult = 
        		_glacierClient.get()
        		.getJobOutput(getJobOutputRequest);

        try( final BufferedReader inBuffRdr = 
        		new BufferedReader(
        				new InputStreamReader( 
        						getJobOutputResult.getBody() ) ); 
           )
        {            
        	
    		LogCustom.obtainLog("job_output", Level.INFO)
    			.info("successfully opened input buffered reader");
        	
        	String inputLine = "";
        	
        	CustomLogger log = LogCustom.obtainLog("job_output", 
        										   Level.INFO);
    		
        	final ObjectMapper mapper = new ObjectMapper();
    		final JsonFactory factory = mapper.getJsonFactory();
    		
    		factory.configure(Feature.AUTO_CLOSE_SOURCE, true);
    		
    		JsonParser parser = factory.createJsonParser( inBuffRdr );
    		
    		while( parser.nextToken() != null )	//null == eof
    		{
    			/* tokename is only retrieved, skip print unless has value */
    			
    			if(   parser.getCurrentName() != null &&
    				! parser.getCurrentName().equals("null") &&
    				
    			  ! parser.getCurrentName()
    			  		  .equals( parser.getText() ) )
    			{
    				_lines.add( parser.getCurrentName() 
    						+ "," + 
    						parser.getText() );

    				log.info( inputLine );

    				log.info(  parser.getCurrentName() 
    						+ "," + 
    						parser.getText() );
    			}//if
    			
    		}//while
    		
        }
        catch( final IOException err )
        {
        	CustomLogger log = LogCustom.obtainLog( 
        			
        				 this.getClass().toString(),
        			
		 				 Level.SEVERE );
        	
        	for( StackTraceElement ele : err.getStackTrace() )
        	{
        		log.severe( ele.toString() );
        		
        	}//for
        		        	
        	Throwables.propagate(err);
        
        }
        finally
        {
        	/* 
        	 * taskOutput is used by addListener, but needs this task
        	 * 
        	 * provide reference to completed task for use by callback 
        	 */
        	
        	_task.get().taskOutput(this);
        	
        }//try
        
		return this;
		
	}//method
	
	
	public ImmutableList<String> lines()
	{
		return ImmutableList.copyOf( _lines );
		
	}//method


	@Fulfills("Runnable")
	@Override
	public void run() 
	{
		retrieveLines();
		
	}//method
	
	
}//class


/**
 * AWS remote job ID.<p>
 *
 */

class JobID extends Observable
{
	private String _id = "";
	
	private final Set<Observer> _watchers = new HashSet<>();
	
	private Boolean _changed = false;
	
	
	public JobID()
	{
		_id = "";
		_changed = false;
		
	}//constructor

	
	public JobID fromString( final String idStr )
	{
		if( !_id.equals(idStr) )
		{ 
			_changed = true;
		
			notifyObservers();
			
		}//if

		_id = idStr;

		return this;
		
	}//method
	

	public Boolean unfinished( final String pathIn ) throws IOException
	{
		return unfinished( Paths.get(pathIn) );
		
	}//method

	
	public Boolean unfinished( final Path pathIn ) throws IOException
	{
		return pathIn.toFile().exists();
		
	}//method
	
	
	public JobID save( final String pathIn ) throws IOException
	{
		return save( Paths.get(pathIn) );
		
	}//method
	
	
	public JobID save( final Path pathIn ) throws IOException
	{
		if( _id.length() < 1 ) throw new AmazonBkpUncheckedException(
				"fail to save aws job id: id not set, zero length");
		
		Files.write( _id , 
					 pathIn.toFile(), 
					 Charset.defaultCharset() );
		
		return this;
		
	}//method


	public JobID restore( final String pathIn ) throws IOException
	{
		return restore( Paths.get(pathIn) );
		
	}//method

	
	public JobID restore( final Path pathIn ) throws IOException
	{
		final String idRead =  
		
				Files.toString( pathIn.toFile(), Charset.defaultCharset() );
		
		if( idRead.length() < 1 ) throw new AmazonBkpUncheckedException(
				"fail to read aws job id from file: id zero length");
		
		_id = idRead;
		
		return this;
		
	}//method
	
	
	public JobID erase( final String pathIn ) throws IOException
	{
		return erase( Paths.get(pathIn) );
		
	}//method

	
	public JobID erase( final Path pathIn ) throws IOException
	{
		if( !pathIn.toFile().delete() ) throw new IOException(
				"fail to delete: " + pathIn);
		
		_id= "";
		
		return this;
		
	}//method
	
	
	@Override
	public String toString()
	{
		return _id;
		
	}//method


	@Override
	public void addObserver( final Observer watcher )
	{
		_watchers.add( watcher );
		
	}//method
	

	@Override	
	public void deleteObserver( final Observer watcher )
	{
		_watchers.remove( watcher );
		
	}//method
	
	
	@Override
	public void deleteObservers()
	{
		_watchers.clear();
		
	}//method

	
	@Override
	public int countObservers()
	{
		return _watchers.size();
		
	}//method
	

	@Override
	protected void setChanged()
	{
		_changed = true;
		
	}//method
	
	
	@Override
	protected void clearChanged()
	{
		_changed = false;
		
	}//method
	

	@Override
	public boolean hasChanged()
	{
		return _changed;
		
	}//method
	

	@Override
	public void notifyObservers()
	{
		if( hasChanged() ) notification();
		
	}//method
	

	@Override
	public void notifyObservers( final Object objIn )
	{
		if( hasChanged() ) notification( objIn );
		
	}//method

	
	private void notification( final Object... objIn )
	{
		Object objOut = "";
		
		if( objIn != null && objIn.length > 0 ) objOut = objIn[0];
		
		final Iterator<Observer> observers = _watchers.iterator();
		
		while( observers.hasNext() )
		{
			observers.next().update( this, objOut );
			
		}//while
		
		clearChanged();		
		
	}//method
	
	
}//class